package cz.proks.bank.manager;

import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import cz.proks.bank.BankConstants;
import cz.proks.bank.dao.PaymentDao;
import cz.proks.bank.domain.Account;
import cz.proks.bank.domain.OperationLog.OperationType;
import cz.proks.bank.domain.Payment;
import cz.proks.bank.domain.Payment.State;
import cz.proks.bank.manager.exeception.AccountNotFoundException;
import cz.proks.bank.manager.exeception.BankException;
import cz.proks.bank.manager.exeception.NotEnoughAvailableMoneyException;
import cz.proks.bank.manager.exeception.PaymentException;
import cz.proks.bank.manager.exeception.PaymentNotFoundException;
import cz.proks.bank.manager.exeception.TransferException;

@Service
public class PaymentManager {
	@Autowired
	private PaymentDao paymentDao;
	
	@Autowired 
	private AccountManager accountManager;
	
	@Autowired
	private CurrencyManager currencyManager;
	
	@Autowired
	private ClearingManager clearingManager;
	
	@Autowired
	private OperationLogManager logManager;
	
	public Payment buildPayment(int sourceAccountNumber, int targetAccountNumber, String sourceBankCode, String targetBankCode, long amount, String currency) throws AccountNotFoundException {				
		Payment payment = new Payment();		
				
		payment.setDate(new Date());
		payment.setSourceAccountId(sourceAccountNumber);
		payment.setTargetAccountId(targetAccountNumber);
		payment.setSourceBankCode(sourceBankCode);
		payment.setTargetBankCode(targetBankCode);
		payment.setAmount(amount);
		payment.setCurrency(currency);
		payment.setPaymentState(State.NEW);
		
		return payment;		
	}
	
	public Payment getPayment(long paymentId) throws PaymentNotFoundException {
		Payment payment = paymentDao.get(paymentId);

		if (payment == null)
			throw new PaymentNotFoundException("Payment " + paymentId + " not found!");

		return payment;
	}
	
	public Payment createPayment(int sourceAccountNumber, int targetAccountNumber, String targetBankCode, long amount, String currency) throws AccountNotFoundException {
		Payment payment = buildPayment(sourceAccountNumber, targetAccountNumber, BankConstants.BANK_CODE, targetBankCode, amount, currency);

		payment = createPayment(payment);
		
		return payment;
	}	
	
	private Payment createPayment(Payment payment) {
		if (payment == null)			
			return null;
		
		payment = paymentDao.create(payment);
		
		return payment;
	}
	
	private Payment updatePayment(Payment payment) {
		if (payment == null)
			return null;
		
		payment = paymentDao.update(payment);
		
		return payment;
	}
	
	public void processPayment(Payment payment) throws PaymentException {
		checkPayment(payment);
		
		if (payment.getId() == 0)
			createPayment(payment);
		
		if (accountManager.isLocalBankAccount(payment.getSourceAccountId(), payment.getSourceBankCode())) 
			deductPayment(payment);
		 else 
			deductRemotePayment(payment);			
		 
		
		if (accountManager.isLocalBankAccount(payment.getTargetAccountId(), payment.getTargetBankCode()))
			creditPayment(payment);
		else 
			creditRemotePayment(payment);
		
		if (payment.getPaymentState() == State.CREDITED_TO_TARGET_ACCOUNT)
			payment.setPaymentState(State.OK);
		
		updatePayment(payment);
	}		
	
	public void checkPayment(Payment payment) throws PaymentException {		
		if (payment == null)
			throw new PaymentException("No payment!");		
		
		if (payment.getSourceAccountId() < 0)
			throw new PaymentException("Source account number mismatches!");
		
		if (payment.getTargetAccountId() < 0)
			throw new PaymentException("Target account number mismatches!");
		
		if (payment.getSourceBankCode() == null	|| payment.getSourceBankCode().isEmpty())
			throw new PaymentException("Source bank code is empty!");
		
		if (payment.getTargetBankCode() == null
				|| payment.getTargetBankCode().isEmpty())
			throw new PaymentException("Target bank code is empty!");

		if (payment.getCurrency() == null || payment.getCurrency().isEmpty())
			throw new PaymentException("Currency is empty!");		
	}	
	
	private void deductPayment(Payment payment) throws PaymentException {				
		checkPayment(payment);		
		
		if (payment.getPaymentState() != State.NEW)
			throw new PaymentException("Payment is processed!");		
		
		try {		
			Account account = accountManager.getAccount(payment.getSourceAccountId());
			
			long ammountToDeduct = currencyManager.convert(payment.getAmount(), payment.getCurrency(), account.getCurrency());		
			
			checkAvailableMoney(account, ammountToDeduct);
			
			account.setAvailableMoney(account.getAvailableMoney() - ammountToDeduct);
					
			payment.setPaymentState(State.DEDUCTED_FROM_SOURCE_ACCOUNT);
			
			accountManager.updateAccount(account);
			
			updatePayment(payment);
			
			logManager.createLog(account, account.getBank(), OperationType.OUTGOING_PAYMENT, new Date(), "Deducted " + payment.getAmount() + " " + payment.getCurrency() + 
																					   " (" + ammountToDeduct + " " + account.getCurrency() + ")");
			
		} catch (BankException e) {
			throw new PaymentException(e);
		}				
	}	
	
	private void deductRemotePayment(Payment payment) throws PaymentException {
		if (payment.getPaymentState() != State.NEW)
			throw new PaymentException("Payment is processed!");
			
		payment.setPaymentState(State.DEDUCTED_FROM_SOURCE_ACCOUNT);					
		
		updatePayment(payment);
	}

	private void creditPayment(Payment payment) throws PaymentException {			
		checkPayment(payment);		
		
		if (payment.getPaymentState() != State.DEDUCTED_FROM_SOURCE_ACCOUNT)
			throw new PaymentException("Payment is processed!");	
		
		try {
			Account account = accountManager.getAccount(payment.getTargetAccountId());
			
			long ammountToCredit = currencyManager.convert(payment.getAmount(), payment.getCurrency(), account.getCurrency());		
									
			account.setAvailableMoney(account.getAvailableMoney() + ammountToCredit);
						
			payment.setPaymentState(State.CREDITED_TO_TARGET_ACCOUNT);
			
			accountManager.updateAccount(account);
			
			updatePayment(payment);
			
			logManager.createLog(account, account.getBank(), OperationType.INGOING_PAYMENT, new Date(), "Credited " + payment.getAmount() + " " + payment.getCurrency() + 
					   " (" + ammountToCredit + " " + account.getCurrency() + ")");
			
		} catch (BankException e) {
			throw new PaymentException(e);
		}						
	}
	
	private void creditRemotePayment(Payment payment) throws PaymentException {
		try {
			clearingManager.transferPayment(payment);
			
			updatePayment(payment);
		} catch (TransferException e) {
			throw new PaymentException(e);
		}
	}
	
	public void checkAvailableMoney(Account account, long moneyToWriteOff) throws NotEnoughAvailableMoneyException {
		if (account == null)
			throw new NotEnoughAvailableMoneyException("No account!");
		
		if (account.getAvailableMoney() - moneyToWriteOff < 0)
			throw new NotEnoughAvailableMoneyException("Cannot write off " + moneyToWriteOff + " "	+ account.getCurrency()	+ " from account " + account.getId() + ". " +
										"Available money are just " + account.getAvailableMoney() + " " + account.getCurrency());
							
	}	
}
